/* @flow */
/**
 * 合并处理: 规范
 * 对于 el、propsData 选项使用默认的合并策略 defaultStrat
 * 对于 data 选项，使用 mergeDataOrFn 函数进行处理，最终结果是 data 选项将变成一个函数，且该函数的执行结果为真正的数据对象
 * 对于 生命周期钩子 选项，将合并成数组，使得父子选项中的钩子函数都能够被执行
 * 对于 directives、filters 以及 components 等资源选项，父子选项将以原型链的形式被处理，正是因为这样我们才能够在任何地方都使用内置组件、指令等
 * 对于 watch 选项的合并处理，类似于生命周期钩子，如果父子选项都有相同的观测字段，将被合并为数组，这样观察者都将被执行
 * 对于 props、methods、inject、computed 选项，父选项始终可用，但是子选项会覆盖同名的父选项字段
 * 对于 provide 选项，其合并策略使用与 data 选项相同的 mergeDataOrFn 函数
 * 最后，以上没有提及到的选项都将使默认选项 defaultStrat
 * 默认合并策略函数 defaultStrat 的策略是：只要子选项不是 undefined 就使用子选项，否则使用父选项
 * 
 */


import config from '../config'
import { warn } from './debug'
import { nativeWatch } from './env'
import { set } from '../observer/index'

import {
  ASSET_TYPES,
  LIFECYCLE_HOOKS
} from 'shared/constants'

import {
  extend,
  hasOwn,
  camelize,
  toRawType,
  capitalize,
  isBuiltInTag,
  isPlainObject
} from 'shared/util'

/**
 * config.optionMergeStrategies 是一个合并选项的策略对象，这个对象下包含很多函数，这些函数就可以认为是合并特定选项的策略
 * 不同的选项可使用不同的合并策略
 */
const strats = config.optionMergeStrategies

/**
 * 选项 el、propsData 的合并策略
 * 仅非生产环境下才存在
 */
if (process.env.NODE_ENV !== 'production') {
  // 非生产环境下在 strats 策略对象上添加两个策略(两个属性)分别是 el 和 propsData，且均为函数 
  // 两个策略函数是用来合并 el 选项和 propsData 选项的
  // 策略函数中的 vm 来自于 mergeOptions 函数的第三个参数
  strats.el = strats.propsData = function (parent, child, vm, key) {
    // el 选项或者 propsData 选项只能在使用 new 操作符创建实例的时候可用
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    // 不传vm这个参数时会发出警告，但代码仍可运行
    // 实际上：不传vm时说明调用者是子组件,如：
    /** 
     * var ChildComponent = {
        el: '#app2',
        created: function () {
          console.log('child component created')
        }
      }
     */
    return defaultStrat(parent, child)
  }
}

/**
 * 合并2个对象： 将from对象合并到to对象中
 */
function mergeData (to: Object, from: ?Object): Object {
  // 没有 from 直接返回 to
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  // 遍历 from 的 key
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    // 如果 from 对象中的 key 不在 to 对象中，则使用 set 函数为 to 对象设置 key 及相应的值，set函数来自于 core/observer/index.js 文件(API Vue.set)
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    // 如果 from 对象中的 key 也在 to 对象中，这两个属性的值不相同且都是纯对象则递归进行深度合并  
    } else if ( toVal !== fromVal && isPlainObject(toVal) &&  isPlainObject(fromVal)  ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  // 2种情况下，最终都是通过调用mergeData函数来实现
  // 是子组件的情况下
  if (!vm) {
    // 在调用 Vue.extend 函数时进行合并处理的，此时父子 data 选项都应该是函数
    // 也说明拿不到 vm 这个参数的时候，合并操作是在 Vue.extend 中进行的
    /** 针对此种情形：子组件data不存在，但父组件中data存在
     * const Parent = Vue.extend({
     *    data: function () {
     *      return {
     *        test: 1
     *      }
     *    }
     *  })
     *  const Child = Parent.extend({})
     * 如果没有子选项则使用父选项，没有父选项就直接使用子选项，且这两个选项都能保证是函数，如果父子选项同时存在，则代码继续进行
     */
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // 当父子选项同时存在时，返回一个函数 mergedDataFn
    return function mergedDataFn () {
      return mergeData(
        // 第一个 this 指定了 data 函数的作用域，而第二个 this 就是传递给 data 函数的参数
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    // vm参数存在时, 处理的是非子组件的选项时 `data` 函数为 `mergedInstanceDataFn` 函数
    return function mergedInstanceDataFn () {
      // childVal：是子组件选项或通过new创建vue实例的选项，其值要么是函数，要么是对象(目的是获取数据对象)
      // 如果是函数就通过执行该函数从而获取到一个纯对象
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      // 如果是函数就通过执行该函数从而获取到一个纯对象 
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        // mergeData函数接收的是2个纯数据对象并最终执行合并策略
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

// 在 strats 策略对象上添加 data 策略函数，用来合并处理 data 选项
// 这样在实例化Vue对象时，data对象就有默认的合并函数了
// 最终都是调用 mergeDataOrFn 函数进行处理的
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  // vm不存在时说明处理的是子组件 
  if (!vm) {
    // 首先判断是否传递了子组件的 data 选项，并且检测 childVal 的类型是不是 function
    // 此处处理逻辑说明： 子组件的data应该是一个函数
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  // 不是子组件的情况下，多传一个参数： vm实例
  return mergeDataOrFn(parentVal, childVal, vm)
}

/**
 * 合并生命周期钩子函数
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  // 由三组三目运算符组成的mergeHook函数
  /**
   * 转换成如下模式: 自右向左解析
   * childVal? (parentVal? parentVal.concat(childVal): (Array.isArray(childVal)? childVal: [childVal])) :parentVal
   * childVal： 类似 vue实例中的created函数， parentVal 应该是 Vue.options.created但 Vue.options.created 是不存在的，所以
   * 最终经过 strats.created 函数的处理将返回一个数组：options.created = [function (){...}];
   * 如果是通过vue.extend创建的实例，则parentVal 为 Parent.options.created，此时会直接合并成一个数组，
   */
  return childVal //是否有 childVal，即判断组件的选项中是否有对应名字的生命周期钩子函数
    ? parentVal //如果有 childVal 则判断是否有 parentVal
      ? parentVal.concat(childVal) //如果有 parentVal 则使用 concat 方法将二者合并为一个数组
      : Array.isArray(childVal) //如果没有 parentVal 则判断 childVal 是不是一个数组(说明钩子函数可以是一个数组)
        ? childVal //如果 childVal 是一个数组则直接返回
        : [childVal] //否则将其作为数组的元素，然后返回数组
    : parentVal //如果没有 childVal 则直接返回 parentVal
}
// LIFECYCLE_HOOKS定义了钩子函数的名称：共11个，
LIFECYCLE_HOOKS.forEach(hook => {
  // 在 strats 策略对象上添加用来合并各个生命周期钩子选项的策略函数，并且这些生命周期钩子选项的策略函数相同：都是 mergeHook 函数
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * 用来合并处理 directives、filters 以及 components 等资源选项
 */
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  // 以 parentVal 为原型创建对象 res（res可以访问到原型上的内置组件，如：KeepAlive,Transition,TransitionGroup）
  const res = Object.create(parentVal || null)
  // 然后判断是否有 childVa
  if (childVal) {
    // 非生产环境下：检测 childVal 是不是一个纯对象的
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    // 如果有的话使用 extend 函数将 childVal 上的属性混合到 res 对象上并返回
    return extend(res, childVal)
  } else {
    return res
  }
}
// ASSET_TYPES为：['component', '', '']
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * 选项 watch 的合并策略
 * 通过在 strats 策略对象上添加 watch 策略函数，用来合并处理watch选项
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // nativeWatch 来自于 core/util/env.js 文件(在 Firefox 中原生提供了 Object.prototype.watch 函数,其它浏览器中时nativeWatch为undefined)
  // 此处的作用是：当发现组件选项是浏览器原生的 watch 时，那说明用户并没有提供 Vue 的 watch 选项，直接重置为 undefined
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  
  // 检测组件选项是否有 watch 选项，没有时则以parentVal为原型创建一个并返回(如果parent有且存在的话)
  if (!childVal) return Object.create(parentVal || null)
  // 非生产环境下使用 assertObjectType 函数对 childVal 进行类型检测，检测其是否是一个纯对象(watch对象必须是一个纯对象)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 如果不存在parentVal(即非子组件时)，则直接使用组件的watch(此时的watch选项可能是数组，也可能是对象)
  if (!parentVal) return childVal

  // 执行到此：说明parentVal及childVal均存在，此时就需执行合并了，此时合并的结果就是数组而不会是对象。
  // 定义 ret 常量，其值为一个对象
  const ret = {}
  // 将 parentVal 的属性混合到 ret 中，后面处理的都将是 ret 对象，最后返回的也是 ret 对象
  extend(ret, parentVal)
  // 遍历 childVal
  // 目的是检测子选项中的值是否也在父选项中，如果在的话将父子选项合并到一个数组，否则直接把子选项变成一个数组返回。
  for (const key in childVal) {
    // 由于遍历的是 childVal，所以 key 是子选项的 key，父选项中未必能获取到值，所以 parent 未必有值
    let parent = ret[key]
    // child 是肯定有值的，因为遍历的就是 childVal 本身
    const child = childVal[key]
    // 如果 parent 存在(说明父子对象中均有)，就将其转为数组
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    // 为ret赋值
    ret[key] = parent
      // 最后，如果 parent 存在，此时的 parent 应该已经被转为数组了，所以直接将 child concat 进去
      ? parent.concat(child)
      // 如果 parent 不存在，直接将 child 转为数组返回
      : Array.isArray(child) ? child : [child]
  }
  // 最终返回新对象 ret: 纯数据对象
  return ret
}

/**
 * 选项 props、methods、inject、computed 的合并策略
 * 在 strats 策略对象上添加 props、methods、inject 以及 computed 策略函数，用来处理同名选项
 */
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // 如果存在 childVal，那么在非生产环境下要检查 childVal 的类型
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // parentVal 不存在的情况下直接返回 childVal
  if (!parentVal) return childVal
  // 如果 parentVal 存在，则创建 ret 对象，然后分别将 parentVal 和 childVal 的属性混合到 ret 中，
  // 注意：由于 childVal 将覆盖 parentVal 的同名属性
  const ret = Object.create(null)
  // 通过2次合并来混合属性
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  // 最后返回 ret 对象
  return ret
}

/**
 * provide 选项的合并策略与 data 选项的合并策略相同，都是使用 mergeDataOrFn 函数
 */
strats.provide = mergeDataOrFn

/**
 * 它是一个默认的策略，当一个选项不需要特殊处理的时候就使用默认的合并策略
 * 
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  // 只要子选项不是 undefined 那么就是用子选项，否则使用父选项
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * 校验组件的名字是否符合要求
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

// 校验名字
export function validateComponentName (name: string) {
  // 限定组件的名字由普通的字符和中横线(-)组成，且必须以字母开头
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  // isBuiltInTag：检测你所注册的组件是否是内置的标签，isReservedTag：值为来自于 platforms/web/util/element.js;检查给定的标签是否是保留的标签，如html和svg
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

/**
 * 规范props参数的格式，方便后面进行参数合并
 * Object-based format.
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // 如果为数组格式时 如props:['name'] 将被规范为 props: { name: { type: null}}
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      // 数组中的元素必须是字符串，否则在开发环境会给出警告
      if (typeof val === 'string') {
        // 将数组的元素传递给 camelize 函数进行驼峰处理 
        name = camelize(val)
        // 转换为对象，默认类型为null
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  // 如果props为对象(2种写法)，则遍历对象，对key进行转驼峰 
  } else if (isPlainObject(props)) {
    // props: {
    //   // 第一种写法，直接写类型
    //   someData1: Number,
    //   // 第二种写法，对象
    //   someData2: {
    //     type: String,
    //     default: ''
    //   }
    // }
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  // props即不是数组也不是对象时： 
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  // 最终规范为对象的形式
  options.props = res
}

/**
 * 规范2.2.0 版本新增的inject： 2种写法(对象和数组)， 该函数作用就是规范这2种写法，统一成纯对象语法
 * 父组件通过 provide 选项向子组件提供数据 父组件提供数据：provide: {data: 'test provide'}
 * 子组件中可以使用 inject 选项注入数据: 子组件获取：inject: ['data'] 或 inject:{d: 'data'}, 在methods中可通过this.data或this.d调用
 */
function normalizeInject (options: Object, vm: ?Component) {
  // 使用 inject 变量缓存 options.inject
  const inject = options.inject
  if (!inject) return
  // 重写 options.inject 的值为一个空的 JSON 对象，将定义空值normalized
  // 修改 normalized 的时候，options.inject 也将受到影响(2者拥有同一个空对象的引用)
  const normalized = options.inject = {}

  // 为数组方式时：转化为对象 inject[i]:{from: inject[i]}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  // 为对象时： 
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      // 直接用原来的key为新对象的key，原来的对象进行规范，规范成：inject：{'data3': { from: 'data3', someProperty: 'someValue' }}
      normalized[key] = isPlainObject(val)
        // 当其值为纯对象时，直接通过extend进行混合
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

/**
 * 规范 directive选项(directives用来注册局部指令)
 * 写法如：test1：对象语法 和test2：函数语法 两种写法
 * directives: {
    test1: {
      bind: function () {
        console.log('v-test1')
      }
    },
    test2: function () {
      console.log('v-test2')
    }
  }
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      // 函数写法时
      if (typeof def === 'function') {
        // 将该函数作为对象形式的 bind 属性和 update 属性的值
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

function assertObjectType (name: string, value: any, vm: ?Component) {
  if (!isPlainObject(value)) {
    warn(
      `Invalid value for option "${name}": expected an Object, ` +
      `but got ${toRawType(value)}.`,
      vm
    )
  }
}

/**
 * 合并两个选项对象为一个新的对象
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 非生产环境下，以 child 为参数调用 checkComponents 方法
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }
  // child 参数除了是普通的选项对象外，还可以是一个函数,如果是函数的话就取该函数的 options 静态属性作为新的 child
  // 如vue的构造函数及Vue.extend创造出的子类均具有options选项
  /**
   * 通过 new Vue创建实例时， child即为 new Vue()中传递的那个对象。
   */
  if (typeof child === 'function') {
    child = child.options
  }
  // 规范 props 选项，props选项有数组和对象2种写法
  normalizeProps(child, vm)
  // 规范 inject选项 2.2+新增(应用层几乎不用)
  normalizeInject(child, vm)
  // 规范 directive选项
  normalizeDirectives(child)
  
  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // child不是vue实例时才进行混合处理
  if (!child._base) {
    // 处理 extends 选项
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    // 处理 mixins 选项:mixins 在 Vue 中用于解决代码复用的问题
    // 任何写在 mixins 中的选项，都会使用 mergeOptions 中相应的合并策略进行处理，这就是 mixins 的实现方式
    if (child.mixins) {
      // mixins 是一个数组所以要遍历一下,
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        // 函数在处理 mixins 选项的时候递归调用了 mergeOptions 函数将 mixins 合并到了 parent 中，并将合并后生成的新对象作为新的 parent
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }
  // 定义一个新的options对象，用来返回，所以说mergeOptions返回了一个新的对象
  const options = {}
  let key
  // 2次循环处理都是将：parent 对象的键作为参数传递给 mergeField 函数
  // key的值类似components、directives、filters 以及 _base
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    // hasOwn用来判断一个属性是否是对象自身的属性(不包括原型上的)
    // 此处用来判断：如果 child 对象的键也在 parent 上出现，那么就不要再调用 mergeField 了
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    // 定义一个常量 strat，它的值是通过指定的 key 访问 strats 对象(值为 config.optionMergeStrategies，上面挂载着与key对应的策略函数)得到
    // 当一个选项没有对应的策略函数时，使用默认策略
    // 策略函数应与需处理的对象同名(通过同名方式调用的)
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  // 最终返回的是一个对象
  return options
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
